;; Copyright (c) Rich Hickey. All rights reserved.
;; The use and distribution terms for this software are covered by the
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this distribution.
;; By using this software in any fashion, you are agreeing to be bound by
;; the terms of this license.
;; You must not remove this notice, or any other, from this software.

(ns cljs.cli
  (:require [clojure.java.io :as io]
            [clojure.string :as string]
            [clojure.edn :as edn]
            [cljs.util :as util]
            [cljs.env :as env]
            [cljs.analyzer :as ana]
            [cljs.analyzer.api :as ana-api]
            [cljs.compiler.api :as comp]
            [cljs.build.api :as build]
            [cljs.closure :as closure]
            [cljs.repl :as repl])
  (:import [java.io File StringReader FileWriter]
           [java.text BreakIterator]
           [java.util Locale]))

(declare main)

;; -----------------------------------------------------------------------------
;; Help String formatting

(def ^{:private true} help-template
  "Usage: java -cp cljs.jar cljs.main [init-opt*] [main-opt] [arg*]

With no options or args, runs an interactive Read-Eval-Print Loop

%s
For --main and --repl:

  - Enters the cljs.user namespace
  - Binds *command-line-args* to a seq of strings containing command line
    args that appear after any main option
  - Runs all init options in order
  - Calls a -main function or runs a repl or script if requested

The init options may be repeated and mixed freely, but must appear before
any main option.

In the case of --compile you may supply --repl or --serve (if applicable)
options afterwards.

Paths may be absolute or relative in the filesystem or relative to
classpath. Classpath-relative paths have prefix of @ or @/")

(defn- auto-fill
  ([ws]
   (auto-fill ws 50))
  ([^String ws max-len]
   (let [b (BreakIterator/getLineInstance Locale/ENGLISH)]
     (.setText b ws)
     (loop [s (.first b) e (.next b) line-len 0 line "" ret []]
       (if (not= e BreakIterator/DONE)
         (let [w (.substring ws s e)
               word-len (.length w)
               line-len (+ line-len word-len)]
           (if (= w "--") ; long-form options are single tokens (i.e. --repl)
             (recur s (.next b) (- line-len 2) line ret)
             (if (> line-len max-len)
               (recur e (.next b) word-len w (conj ret line))
               (recur e (.next b) line-len (str line w) ret))))
         (conj ret (str line (.substring ws s (.length ws)))))))))

(defn- opt->str [cs {:keys [arg doc]}]
  (letfn [(desc-string [filled]
            (string/join "\n"
              (map #(apply str (concat (repeat 6 "     ") [%]))
                filled)))]
    (let [[f & r] cs

          fstr (cond-> (if (= 1 (count cs))
                         (str "   " f)
                         (format "%1$5s" f))
                 (not (empty? r)) (str ", " (string/join ", " r))
                 arg (str " " arg))
          filled (auto-fill doc)]
      (if (< (.length fstr) 30)
        (cond-> (str (format "%1$-30s" fstr) (first filled) "\n")
          (seq (rest filled)) (str (desc-string (rest filled)) "\n"))
        (str
          fstr "\n"
          (desc-string fstr) "\n")))))

(defn- group->str [options group]
  (let [{:keys [desc pseudos]} (get-in options [:groups group])]
    (apply str
      desc ":\n"
      (->> (:init options)
        (filter (fn [[k v]] (= (:group v) group)))
        (concat pseudos)
        (sort-by ffirst)
        (map (fn [[k v]] (opt->str k v)))))))

(defn- primary-groups-str [options]
  (str
    (group->str options ::main&compile) "\n"
    (group->str options ::main) "\n"
    (group->str options ::compile) "\n"))

(defn- all-groups-str [{:keys [groups] :as options}]
  (let [custom-groups
        (disj (set (keys groups))
          ::main&compile ::main ::compile)]
    (apply str
      (primary-groups-str options)
      (map
        (fn [group]
          (str (group->str options group) "\n"))
        custom-groups))))

(defn- main-str [options]
  (let [pseudos {["path"] {:doc "Run a script from a file or resource"}
                 ["-"] {:doc "Run a script from standard input"}}]
    (apply str
      "main options:\n"
      (->> (:main options)
        (concat pseudos)
        (sort-by ffirst)
        (remove (fn [[k v]] (nil? (ffirst k))))
        (map (fn [[k v]] (opt->str k v)))))))

(defn- options-str [options]
  (str
    (all-groups-str options)
    (main-str options)))

(declare merged-commands)

(defn help-str [repl-env]
  (format help-template
    (options-str (merged-commands repl-env))))

;; -----------------------------------------------------------------------------
;; Main

(defn- output-dir-opt
  [cfg output-dir]
  (assoc-in cfg [:options :output-dir] output-dir))

(defn- verbose-opt
  [cfg value]
  (assoc-in cfg [:options :verbose] (= value "true")))

(defn- validate-watch-paths [[path :as paths]]
  (when (or (nil? path)
            (and (not (.exists (io/file path)))
                 (or (string/blank? path)
                     (string/starts-with? path "-"))))
    (throw
      (ex-info
        (str "Missing watch path(s)")
        {:cljs.main/error :invalid-arg})))
  (when-let [non-existent (seq (remove #(.exists (io/file %)) paths))]
    (throw
      (ex-info
        (if (== 1 (count non-existent))
          (str "Watch path "
               (first non-existent)
               " does not exist")
          (str "Watch paths "
               (string/join ", " (butlast non-existent))
               " and "
               (last non-existent)
               " does not exist"))
        {:cljs.main/error :invalid-arg}))))

(defn- watch-opt
  [cfg paths]
  (let [paths (util/split-paths paths)]
    (validate-watch-paths paths)
    (update-in cfg [:options :watch] (fnil into []) paths)))

(defn- optimize-opt
  [cfg level]
  (assoc-in cfg [:options :optimizations] (keyword level)))

(defn- output-to-opt
  [cfg path]
  (assoc-in cfg [:options :output-to] path))

(defn- deps-cmd-opt
  [cfg deps-cmd]
  (assoc-in cfg [:options :deps-cmd] deps-cmd))

(defn- target-opt
  [cfg target]
  (let [target (if (= "node" target) "nodejs" target)]
    (assoc-in cfg [:options :target] (keyword target))))

(defn missing-file [x]
  (throw
    (ex-info
      (str "File " x " does not exist")
      {:cljs.main/error :invalid-arg})))

(defn missing-resource [x]
  (throw
    (ex-info
      (str "Resource "
        (if (string/starts-with? x "@/")
          (subs x 2)
          (subs x 1))
        " does not exist")
      {:cljs.main/error :invalid-arg})))

(defn read-edn-opts [str]
  (letfn [(read-rsrc [rsrc-str orig-str]
            (if-let [rsrc (io/resource rsrc-str)]
              (edn/read-string (slurp rsrc))
              (missing-resource orig-str)))]
    (cond
     (string/starts-with? str "@/") (read-rsrc (subs str 2) str)
     (string/starts-with? str "@") (read-rsrc (subs str 1) str)
     :else
     (let [f (io/file str)]
       (if (.exists f)
         (edn/read-string (slurp f))
         (missing-file str))))))

(defn load-edn-opts [str]
  (reduce merge {} (map read-edn-opts (util/split-paths str))))

(defn- repl-env-opts-opt
  [cfg ropts]
  (let [ropts (string/trim ropts)
        edn   (if (string/starts-with? ropts "{")
                (edn/read-string ropts)
                (load-edn-opts ropts))]
    (update cfg :repl-env-options merge edn)))

(defn- compile-opts-opt
  [cfg copts]
  (let [copts (string/trim copts)
        edn   (if (string/starts-with? copts "{")
                (edn/read-string copts)
                (load-edn-opts copts))]
    (update cfg :options merge edn)))

(defn- init-opt
  [cfg file]
  (let [file' (cond
                (string/starts-with? file "@/")
                (io/resource (subs file 2))
                (string/starts-with? file "@")
                (io/resource (subs file 1))
                :else
                (let [f (io/file file)]
                  (if (.exists f)
                    f
                    (missing-file file))))]
    (when-not file'
      (missing-resource file))
    (update-in cfg [:inits]
      (fnil conj [])
      {:type :init-script
       :script file'})))

(defn- eval-opt
  [cfg form-str]
  (update-in cfg [:inits]
    (fnil conj [])
    {:type :eval-forms
     :forms (ana-api/forms-seq (StringReader. form-str))}))

(defn get-dispatch
  ([commands k opt]
    (get-dispatch commands k opt nil))
  ([commands k opt default]
   (let [k' (keyword (str (name k) "-dispatch"))]
     (or (get-in commands [k' opt]) default))))

(defn initialize
  "Common initialize routine for repl, script, and null opts"
  [inits commands]
  (reduce
    (fn [ret [opt arg]]
      ((get-dispatch commands :init opt) ret arg))
    {} inits))

(defn temp-out-dir []
  (let [f (File/createTempFile "out" (Long/toString (System/nanoTime)))]
    (.delete f)
    (util/mkdirs f)
    (util/path f)))

(defn- repl-name [repl-env]
  (let [repl-ns (-> repl-env meta :ns str)]
    (when (string/starts-with? repl-ns "cljs.repl.")
      (subs repl-ns (count "cljs.repl.")))))

(defn- fast-initial-prompt? [repl-env options inits]
  (boolean
    (and (empty? inits)
         (not (:verbose options))
         (not (:repl-verbose options))
         (contains? #{"node"} (repl-name repl-env)))))

(defn target->repl-env [target default]
  (if (= :nodejs target)
    (do
      (require 'cljs.repl.node)
      (resolve 'cljs.repl.node/repl-env))
    default))

(defn- repl-opt
  "Start a repl with args and inits. Print greeting if no eval options were
present"
  [repl-env [_ & args] {:keys [repl-env-options options inits] :as cfg}]
  (let [opts   (cond-> options
                 (not (:output-dir options))
                 (assoc :output-dir (temp-out-dir) :temp-output-dir? true)
                 (not (contains? options :aot-cache))
                 (assoc :aot-cache true))
        reopts (merge repl-env-options (select-keys opts [:main :output-dir]))
        _      (when (or ana/*verbose* (:verbose opts))
                 (util/debug-prn "REPL env options:" (pr-str reopts)))
        renv   (apply (target->repl-env (:target options) repl-env) (mapcat identity reopts))]
    (repl/repl* renv
      (assoc opts
        ::repl/fast-initial-prompt?
        (or (fast-initial-prompt? repl-env options inits)
            (::repl/fast-initial-prompt? (repl/repl-options renv)))

        :quit-prompt
        (if (empty? inits)
          repl/repl-title
          (constantly nil))

        :inits
        (into
          [{:type :init-forms
            :forms (when-not (empty? args)
                     [`(set! *command-line-args* (list ~@args))])}]
          inits)))))

(defn default-main
  "Default handler for the --main flag. Will start REPL, invoke -main with the
  supplied arguments."
  [repl-env {:keys [main script args repl-env-options options inits] :as cfg}]
  (let [opts   (cond-> options
                 (not (:output-dir options))
                 (assoc :output-dir (temp-out-dir) :temp-output-dir? true)
                 (not (contains? options :aot-cache))
                 (assoc :aot-cache true))
        reopts (merge repl-env-options
                 (select-keys opts [:output-to :output-dir]))
        _      (when (or ana/*verbose* (:verbose opts))
                 (util/debug-prn "REPL env options:" (pr-str reopts)))
        renv   (apply (target->repl-env (:target options) repl-env) (mapcat identity reopts))
        coptsf (when-let [od (:output-dir opts)]
                 (io/file od "cljsc_opts.edn"))
        copts  (when (and coptsf (.exists coptsf))
                 (edn/read-string (slurp coptsf)))
        opts   (merge copts
                 (build/add-implicit-options
                   (merge (repl/repl-options renv) opts)))]
    (binding [env/*compiler*   (env/default-compiler-env opts)
              ana/*cljs-ns*    'cljs.user
              repl/*repl-opts* opts
              ana/*verbose*    (:verbose opts)
              repl/*repl-env*  renv]
      (when ana/*verbose*
        (util/debug-prn "Compiler options:" (pr-str repl/*repl-opts*)))
      (comp/with-core-cljs repl/*repl-opts*
        (fn []
          (try
            (repl/setup renv repl/*repl-opts*)
            ;; Load cljs.repl runtime (so ex-str, ex-triage, etc. are available)
            (repl/evaluate-form renv (ana-api/empty-env) "<cljs repl>"
              `(~'require ~''cljs.repl))
            ;; REPLs don't normally load cljs_deps.js
            (when (and coptsf (.exists coptsf))
              (let [depsf (io/file (:output-dir opts) "cljs_deps.js")]
                (when (.exists depsf)
                  (repl/evaluate renv "cljs_deps.js" 1 (slurp depsf)))))
            (repl/evaluate-form renv (ana-api/empty-env) "<cljs repl>"
              (when-not (empty? args)
                `(set! *command-line-args* (list ~@args))))
            (repl/evaluate-form renv (ana-api/empty-env) "<cljs repl>"
              `(~'ns ~'cljs.user))
            (repl/maybe-install-npm-deps opts)
            (repl/run-inits renv inits)
            (when script
              (cond
                (= "-" script)
                (repl/load-stream renv "<cljs repl>" *in*)

                (.exists (io/file script))
                (with-open [stream (io/reader script)]
                  (repl/load-stream renv script stream))

                (string/starts-with? script "@/")
                (if-let [rsrc (io/resource (subs script 2))]
                  (repl/load-stream renv (util/get-name rsrc) rsrc)
                  (missing-resource script))

                (string/starts-with? script "@")
                (if-let [rsrc (io/resource (subs script 1))]
                  (repl/load-stream renv (util/get-name rsrc) rsrc)
                  (missing-resource script))

                (string/starts-with? script "-")
                (throw
                  (ex-info
                    (str "Expected script or -, got flag " script " instead")
                    {:cljs.main/error :invalid-arg}))

                :else
                (throw
                  (ex-info
                    (str "Script " script " does not exist")
                    {:cljs.main/error :invalid-arg}))))
            (when main
              (let [src (build/ns->source main)]
                (when-not src
                  (throw
                    (ex-info
                      (str "Namespace " main " does not exist."
                        (when (string/includes? main "-")
                          " Please check that namespaces with dashes use underscores in the ClojureScript file name."))
                      {:cljs.main/error :invalid-arg})))
                (repl/load-stream renv (util/get-name src) src)
                (repl/evaluate-form renv (ana-api/empty-env) "<cljs repl>"
                  `(~(symbol (name main) "-main") ~@args))))
            (finally
              (repl/tear-down renv))))))))

(defn- main-opt
  "Call the -main function from a namespace with string arguments from
  the command line. Can be customized with ::cljs.cli/main fn entry in
  the map returned by cljs.repl/IReplEnvOptions. For default behavior
  see default-main."
  [repl-env [_ ns & args] cfg]
  ((::main (repl/repl-options (repl-env)) default-main)
    repl-env (merge cfg {:main ns :args args})))

(defn- null-opt
  "No repl or script opt present, just bind args and run inits"
  [repl-env args cfg]
  ((::main (repl/repl-options (repl-env)) default-main)
    repl-env (merge cfg {:args args})))

(defn- help-opt
  [repl-env _ _]
  (println (help-str repl-env)))

(defn- script-opt
  "If no main option was given (compile, repl, main), handles running in
  'script' mode. Can be customized with ::cljs.cli/main fn entry in
  the map returned by cljs.repl/IReplEnvOptions. For default behavior see
  default-main."
  [repl-env [path & args] cfg]
  ((::main (repl/repl-options (repl-env)) default-main)
    repl-env (merge cfg {:script path :args args})))

(defn watch-proc [cenv path opts]
  (let [log-file (io/file (util/output-directory opts) "watch.log")]
    (util/mkdirs log-file)
    (#'repl/err-out (println "Watch compilation log available at:" (str log-file)))
    (let [log-out (FileWriter. log-file)]
      (binding [*err* log-out
                *out* log-out]
        (build/watch path (dissoc opts :watch) cenv)))))

(defn- serve-opt
  [_ [_ address-port & args] {:keys [options] :as cfg}]
  (let [[host port] (if address-port
                      (string/split address-port #":")
                      ["localhost" 9000])]
    (require 'cljs.repl.browser)
    ((ns-resolve 'cljs.repl.browser 'serve)
      {:host host
       :port (if port
               (cond-> port (string? port) Integer/parseInt)
               9000)
       :output-dir (:output-dir options "out")})))

(defn- install-deps-opt
  [_ _ {:keys [options] :as cfg}]
  (closure/maybe-install-node-deps! options))

(defn get-main-ns [{:keys [ns options] :as cfg}]
  (if (and ns (not (#{"-r" "--repl" "-s" "--serve"} ns)))
    (symbol ns)
    (:main options)))

(defn default-compile
  [repl-env {:keys [ns args options post-compile-fn] :as cfg}]
  (let [rfs      #{"-r" "--repl"}
        sfs      #{"-s" "--serve"}
        env-opts (repl/repl-options ((target->repl-env (:target options) repl-env)))
        repl?    (boolean (or (rfs ns) (rfs (first args))))
        serve?   (boolean (or (sfs ns) (sfs (first args))))
        main-ns  (get-main-ns cfg)
        opts     (as->
                   (merge
                     (select-keys env-opts
                       (cond-> closure/known-opts
                         repl? (conj :browser-repl)))
                     options
                     (when main-ns
                       {:main main-ns})) opts
                   (cond-> opts
                     (not (:output-to opts))
                     (assoc :output-to
                       (.getPath (io/file (:output-dir opts "out") "main.js")))

                     (= :advanced (:optimizations opts))
                     (dissoc :browser-repl)

                     (not (:output-dir opts))
                     (assoc :output-dir "out")

                     (not (contains? opts :aot-cache))
                     (assoc :aot-cache true)

                     (sequential? (:watch opts))
                     (update :watch cljs.closure/compilable-input-paths)))
        convey   (into [:output-dir] repl/known-repl-opts)
        cfg      (update cfg :options merge (select-keys opts convey))
        source   (when (and (= :none (:optimizations opts :none)) main-ns)
                   (closure/check-main opts)
                   (:uri (build/ns->location main-ns)))
        cenv     (env/default-compiler-env
                   (closure/add-externs-sources (dissoc opts :foreign-libs)))]
    (env/with-compiler-env cenv
      (if-let [path (:watch opts)]
        (if repl?
          (build/build source opts cenv)
          (build/watch path opts cenv))
        (build/build source opts cenv))
      (when (fn? post-compile-fn)
        (post-compile-fn))
      (when repl?
        (repl-opt repl-env args
          (cond-> (assoc-in cfg [:options :compiler-env] cenv)
            main-ns (update :options merge {:main main-ns}))))
      (when serve?
        (serve-opt repl-env args cfg)))))

(defn- compile-opt
  "Handle the compile flag. Custom compilation is possible by providing
  :cljs.cli/compile fn in the map returned by cljs.repl/IReplEnvOptions.
  For default behavior see default-compile."
  [repl-env [_ ns & args] cfg]
  ((::compile (repl/-repl-options (repl-env)) default-compile)
    repl-env (merge cfg {:args args :ns ns})))

(defn get-options
  "Given a commands map and a phase (:init or :main), return all flags
  which can be handled as a set. If phase is :all will return the entire
  flag set (:init + :main)."
  [commands phase]
  (if (= :all phase)
    (into (get-options commands :main) (get-options commands :init))
    (-> (get commands (keyword (str (name phase) "-dispatch")))
      keys set)))

(defn get-flags-set
  "See get-options, this just provides a better name."
  [commands phase]
  (get-options commands phase))

(defn bool-init-options
  [commands]
  (reduce
    (fn [ret [flags config]]
      (cond-> ret
        (= "bool" (:arg config))
        (into flags)))
    #{} (:init commands)))

(defn dispatch?
  "Given a commands map, a phase (:init or :main) and a command line flag,
  return true if the flag has a handler."
  [commands phase opt]
  (contains? (get-flags-set commands phase) opt))

(defn add-commands
  "Given commands map (see below), create a commands map with :init-dispatch
  and :main-dispatch keys where short and long arguments are mapped individually
  to their processing fn."
  ([commands]
    (add-commands {:main-dispatch nil :init-dispatch nil} commands))
  ([commands {:keys [groups main init]}]
   (letfn [(merge-dispatch [commands dispatch-key options]
             (update-in commands [dispatch-key]
               (fn [m]
                 (reduce
                   (fn [ret [flag-names flag-config]]
                     (merge ret
                       (zipmap flag-names (repeat (:fn flag-config)))))
                   m options))))]
     (-> commands
       (update-in [:groups] merge groups)
       (update-in [:main] merge main)
       (update-in [:init] merge init)
       (merge-dispatch :init-dispatch init)
       (merge-dispatch :main-dispatch main)))))

(def ^{:doc "Default commands for ClojureScript REPLs. :groups are to support
printing organized output for --help. a :main option must come at the end, they
specify things like running a -main fn, compile, repl, or web serving. Sometimes
:main options can be used together (i.e. --compile --repl), but this is not
generic - the combinations must be explicitly supported"}
  default-commands
  (add-commands
    {:groups {::main&compile {:desc "init options"
                              :pseudos
                              {["-re" "--repl-env"]
                               {:arg "env"
                                :doc (str "The REPL environment to use. Built-in "
                                          "supported values: node, browser. "
                                          "Defaults to browser. If given a "
                                          "non-single-segment namespace, will "
                                          "use the repl-env fn found there.")}}}
              ::main {:desc "init options only for --main and --repl"}
              ::compile {:desc "init options only for --compile"}}
     :init
     {["-i" "--init"]          {:group ::main :fn init-opt
                                :arg "path"
                                :doc "Load a file or resource"}
      ["-e" "--eval"]          {:group ::main :fn eval-opt
                                :arg "string"
                                :doc "Evaluate expressions in string; print non-nil values"}
      ["-v" "--verbose"]       {:group ::main :fn verbose-opt
                                :arg "bool"
                                :doc "If true, will enable ClojureScript verbose logging"}
      ["-d" "--output-dir"]    {:group ::main&compile :fn output-dir-opt
                                :arg "path"
                                :doc (str "Set the output directory to use. If "
                                       "supplied, cljsc_opts.edn in that directory "
                                       "will be used to set ClojureScript compiler "
                                       "options") }
      ["-w" "--watch"]         {:group ::compile :fn watch-opt
                                :arg "paths"
                                :doc (str "Continuously build, only effective with the "
                                          "--compile main option. Specifies a system-dependent "
                                          "path-separated list of directories to watch.")}
      ["-o" "--output-to"]     {:group ::compile :fn output-to-opt
                                :arg "file"
                                :doc "Set the output compiled file"}
      ["--deps-cmd"]           {:group ::compile :fn deps-cmd-opt
                                :arg "string"
                                :doc "Set the node dependency manager. Only npm or yarn supported"}
      ["-O" "--optimizations"] {:group ::compile :fn optimize-opt
                                :arg "level"
                                :doc
                                (str "Set optimization level, only effective with "
                                  "--compile main option. Valid values are: none, "
                                  "whitespace, simple, advanced")}
      ["-t" "--target"]        {:group ::main&compile :fn target-opt
                                :arg "name"
                                :doc
                                (str "The JavaScript target. Configures environment bootstrap and "
                                     "defaults to browser. Supported values: node or nodejs, "
                                     "webworker, bundle, none") }
      ["-ro" "--repl-opts"]    {:group ::main&compile :fn repl-env-opts-opt
                                :arg "edn"
                                :doc (str "Options to configure the repl-env, can be an EDN string or "
                                          "system-dependent path-separated list of EDN files / classpath resources. Options "
                                          "will be merged left to right.")}
      ["-co" "--compile-opts"] {:group ::main&compile :fn compile-opts-opt
                                :arg "edn"
                                :doc (str "Options to configure the build, can be an EDN string or "
                                          "system-dependent path-separated list of EDN files / classpath resources. Options "
                                          "will be merged left to right.")}}
     :main
     {["--install-deps"]       {:fn install-deps-opt
                                :doc "Install all :npm-deps found upstream and in supplied compiler options"}
      ["-r" "--repl"]          {:fn repl-opt
                                :doc "Run a repl"}
      ["-m" "--main"]          {:fn main-opt
                                :arg "ns"
                                :doc "Call the -main function from a namespace with args"}
      ["-c" "--compile"]       {:fn compile-opt
                                :arg "[ns]"
                                :doc (str "Run a compile. If optional namespace specified, use as "
                                          "the main entry point. If --repl follows, "
                                          "will launch a REPL after the compile completes. "
                                          "If --serve follows, will start a web server that serves "
                                          "the current directory after the compile completes.")}
      ["-s" "--serve"]         {:fn serve-opt
                                :arg "host:port"
                                :doc (str "Start a simple web server to serve the current directory")}
      [nil]                    {:fn null-opt}
      ["-h" "--help" "-?"]     {:fn help-opt
                                :doc "Print this help message and exit"}}}))

(defn normalize
  "Given a commands map (flag + value -> option processor fn) and the sequence of
  command line arguments passed to the process, normalize it. Boolean flags don't
  need to specify anything, insert the implied trues and return the normalized
  command line arguments."
  [commands args]
  (letfn [(normalize* [args*]
            (if (not (contains? (get-flags-set commands :main) (first args*)))
              (let [pred (complement (bool-init-options commands))
                    [pre post] ((juxt #(take-while pred %)
                                  #(drop-while pred %))
                                args*)]
                (cond
                  (= pre args*) pre

                  (not (#{"true" "false"} (fnext post)))
                  (concat pre [(first post) "true"]
                    (normalize commands (next post)))

                  :else
                  (concat pre [(first post) (fnext post)]
                    (normalize commands (nnext post)))))
              args*))]
    (loop [args args args' (normalize* args)]
      (if (= args args')
        args'
        (recur args' (normalize* args'))))))

(defn merged-commands
  "Given a repl environment combine the default commands with the custom
  REPL commands. Commands are a mapping from a command line argument
  (flag + value) to a function to handle that particular flag + value."
  [repl-env]
  (add-commands default-commands
    (::commands (repl/repl-options (repl-env)))))

(defn main
  "A generic runner for ClojureScript. repl-env must satisfy
  cljs.repl/IReplEnvOptions and cljs.repl/IJavaScriptEnv protocols. args is a
  sequence of command line flags."
  [repl-env & args]
  (try
    (let [commands (merged-commands repl-env)]
      (if args
        (loop [[opt arg & more :as args] (normalize commands args) inits []]
          (if (dispatch? commands :init opt)
            (recur more (conj inits [opt arg]))
            ((get-dispatch commands :main opt script-opt)
              repl-env args (initialize inits commands))))
        (repl-opt repl-env nil nil)))
    (finally
      (flush))))
